package server.java.autoduel.World;

import java.awt.Point;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.sql.Time;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;

import server.java.autoduel.Server;
import server.java.autoduel.Locations.Arena;
import server.java.autoduel.Locations.Location;
import server.java.autoduel.Locations.TruckStop;
import server.java.autoduel.Network.IPacketizable;
import server.java.autoduel.Network.OPCODES;
import server.java.autoduel.Network.Util;
import server.java.autoduel.Player.Player;

import com.sun.sgs.app.AppContext;
import com.sun.sgs.app.Channel;
import com.sun.sgs.app.ChannelListener;
import com.sun.sgs.app.ChannelManager;
import com.sun.sgs.app.ClientSession;
import com.sun.sgs.app.DataManager;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.ManagedReference;
import com.thoughtworks.xstream.XStream;

/**
 * Represents a grid, which houses locations and players, in the autoduel world
 * 
 * @author Ian Axelrod
 * @author Liz Ha
 * @author Andrew Muldowney
 * @author Edward Futch
 */
public class Grid implements Serializable, ManagedObject, IPacketizable,
		ChannelListener {
	private static final long serialVersionUID = 3129625616048999859L;

	/** The {@link Logger} for this class. */
	static final Logger logger = Logger.getLogger(Grid.class.getName());

	/**
	 * How large is a grid. Ratio 16:9
	 */
	public static Integer Width = 512;
	public static Integer Height = 288;

	/**
	 * Players present in this grid
	 */
	private Set<ManagedReference<Player>> playerRefs;

	/**
	 * Locations present in this grid
	 */
	private Set<ManagedReference<Location>> locationRefs;

	/**
	 * A link to the background image on this grid
	 */
	private String background;

	/**
	 * The path to the xml file that holds the locations for this grid
	 */
	private String locsPath;

	/**
	 * The path to the xml file that holds the objects for this grid
	 */
	private String objsPath;

	/**
	 * The coordinate of this grid in the world
	 */
	private Point coord;

	/**
	 * The coordinates local coordinates of this grid
	 */
	private Point localCoord;

	/**
	 * Is this grid the requesting player's grid?
	 */
	private byte isPlayerGrid;

	/**
	 * The index of this grid in the player's world
	 */
	private Point localIndex = new Point(1, 1);

	/**
	 * Default constructor
	 */
	public Grid() {
		Grid.logger.setLevel(Server.loggerLevel);
	}

	/**
	 * Constructs a grid
	 * 
	 * @param background
	 *            the background image for this grid
	 * @param locsPath
	 *            the path to the xml file holding the locations for this grid
	 * @param objsPath
	 *            the path to the xml file holding the objects for this grid
	 */
	public Grid(String background, String locsPath, String objsPath, Point coord) {
		Grid.logger.setLevel(Server.loggerLevel);
		this.background = background;
		this.locsPath = locsPath;
		this.objsPath = objsPath;
		this.coord = coord;
	}

	/**
	 * Add a player to this grid
	 * 
	 * @param player
	 *            the player to add to this grid
	 * @return true if the player was added
	 */
	public boolean addPlayer(Player player) {
		final DataManager dataManager = AppContext.getDataManager();
		dataManager.markForUpdate(this);

		if (this.playerRefs == null) {
			this.playerRefs = new HashSet<ManagedReference<Player>>();
		}

		// add this player to our channel
		AppContext.getChannelManager().getChannel(this.coord.toString())
				.join(player.getSession());

		Grid.logger.info("Player " + player.toString() + " added to grid "
				+ toString());

		// add this player to our set of player references
		return this.playerRefs.add(dataManager.createReference(player));
	}

	/**
	 * Returns a copy of this grid
	 * 
	 * @return a copy of this grid
	 */
	public Grid copy() {
		return new Grid(this.background, this.locsPath, this.objsPath,
				this.coord);
	}

	@Override
	public ByteBuffer encodePacket(OPCODES opcode) {
		final byte[] bytes = new byte[1 + // opcode
				4 + // Height
				4 + // Width
				this.background.length() + 1 + // delim
				4 + // coordx
				4 + // coordy
				4 + // local index x
				4 + // local index y
				1 + // player grid?
				1]; // delim
		final ByteBuffer buf = ByteBuffer.wrap(bytes);

		buf.put((byte) OPCODES.ORD_UPDGRID.ordinal());
		buf.putInt(Grid.Height);
		buf.putInt(Grid.Width);
		buf.put(this.background.getBytes());
		buf.put(Util.PKT_DLM);
		buf.putInt(this.coord.x);
		buf.putInt(this.coord.y);
		buf.putInt(this.localIndex.x);
		buf.putInt(this.localIndex.y);
		buf.put(this.isPlayerGrid);
		buf.put(Util.PKT_DLM);
		buf.flip();

		return buf;
	}

	/**
	 * @return the coord
	 */
	public Point getCoord() {
		return this.coord;
	}

	/**
	 * @return the isPlayerGrid
	 */
	public byte getIsPlayerGrid() {
		return this.isPlayerGrid;
	}

	/**
	 * @return the localCoord
	 */
	public Point getLocalCoord() {
		return this.localCoord;
	}

	/**
	 * @return the localIndex
	 */
	public Point getLocalIndex() {
		return this.localIndex;
	}

	/**
	 * Converts the location refs to a regular array of locations
	 * 
	 * @return the array of locations
	 */
	public Set<Location> getLocations() {
		final Set<Location> locations = new HashSet<Location>();

		if (this.locationRefs == null) {
			this.locationRefs = new HashSet<ManagedReference<Location>>();
		}

		for (final ManagedReference<Location> locationRef : this.locationRefs) {
			locations.add(locationRef.get());
		}

		return locations;
	}

	/**
	 * Get the players on this grid
	 * 
	 * @return the players on this grid
	 */
	public Player[] getPlayers() {
		final ArrayList<Player> players = new ArrayList<Player>();

		if (this.playerRefs != null) {
			for (final ManagedReference<Player> playerRef : this.playerRefs) {
				players.add(playerRef.get());
			}
		}

		Player[] playersArr = new Player[players.size()];

		playersArr = players.toArray(playersArr);

		return playersArr;
	}

	/**
	 * Checks to see if this grid has no background
	 * 
	 * @return
	 */
	public boolean isEmptyGrid() {
		return ((this.background == null) || this.background.isEmpty());
	}

	/**
	 * Takes a set of locations from the specified xml file and creates
	 * references to them
	 * 
	 * @param currentDir
	 *            the current directory
	 */
	public void loadLocs(String currentDir) {
		Grid.logger.info("Loading Locs");
		final File locFile = new File(currentDir + this.locsPath);
		FileInputStream reader;
		try {
			reader = new FileInputStream(locFile);
		} catch (final FileNotFoundException e) {
			Grid.logger.info("*FileNotFound*: " + currentDir + this.locsPath);
			return;
		}
		final byte[] buf = new byte[(int) locFile.length()];

		try {
			reader.read(buf);
			reader.close();
		} catch (final IOException e) {
			return;
		}

		final XStream xstream = new XStream();
		xstream.alias("TruckStop", TruckStop.class);
		xstream.alias("Arena", Arena.class);
		xstream.alias("hourOpen", Time.class);
		xstream.alias("hourClose", Time.class);

		final DataManager dataManager = AppContext.getDataManager();

		dataManager.markForUpdate(this);

		final Object[] locations = (Object[]) xstream.fromXML(new String(buf));

		if (this.locationRefs == null) {
			this.locationRefs = new HashSet<ManagedReference<Location>>();
		}

		for (final Object locObj : locations) {
			((Location) locObj).loadMembers();
			this.locationRefs.add((ManagedReference<Location>) dataManager
					.createReference((Location)locObj));
		}

		Grid.logger.info("Loaded locations from xml: " + this.locsPath);
	}

	/**
	 * Takes a set of objects from the specified xml file and creates references
	 * to them
	 * 
	 * @param currentDir
	 *            the current directory
	 * @throws IOException
	 */
	public void loadObjs(String currentDir) throws IOException {
		// TODO: Add object loading
	}

	@Override
	public void receivedMessage(Channel arg0, ClientSession arg1,
			ByteBuffer arg2) {
		// Not needed, clients send messages by their own private connection
	}

	/**
	 * Remove a player from this grid
	 * 
	 * @param player
	 *            the player to remove from this grid
	 * @return true if the player was removed/didn't exist
	 */
	public boolean removePlayer(Player player) {
		final DataManager dataManager = AppContext.getDataManager();
		dataManager.markForUpdate(this);

		// remove this player from our channel
		final ClientSession session = player.getSession();
		// If the client doesn't have a session, then he has already been booted
		// off the channel, so there is no need to remove him explicitly
		if (session != null) {
			final ChannelManager channelManager = AppContext
					.getChannelManager();

			channelManager.getChannel(this.coord.toString()).leave(session);
		}
		Grid.logger.info("Player " + player.toString() + " removed from grid "
				+ toString());

		// remove this player from our set of player references
		if (this.playerRefs != null) {
			return this.playerRefs.remove(dataManager.createReference(player));
		} else {
			return true;
		}
	}

	/**
	 * Send a message to all players on this grid
	 * 
	 * @param message
	 *            the message to be sent
	 */
	public void sendMessage(ByteBuffer message) {
		final ChannelManager channelManager = AppContext.getChannelManager();

		channelManager.getChannel(this.coord.toString()).send(message);
	}

	/**
	 * Send a message to all players in this grid except the specified player
	 * 
	 * @param message
	 *            message to send
	 * @param player
	 *            player not to send the message to
	 */
	public void sendMessageExempting(ByteBuffer message, Player player) {
		if (this.playerRefs != null) {
			for (final ManagedReference<Player> tmpPlayerRef : this.playerRefs) {
				final Player tmpPlayer = tmpPlayerRef.get();

				if (!tmpPlayer.equals(player)) {
					tmpPlayer.sendMessage(message);
				}
			}
		}
	}

	/**
	 * @param coord
	 *            the coord to set
	 */
	public void setCoord(Point coord) {
		AppContext.getDataManager().markForUpdate(this);
		this.coord = coord;
	}

	/**
	 * @param isPlayerGrid
	 *            the isPlayerGrid to set
	 */
	public void setIsPlayerGrid(byte isPlayerGrid) {
		AppContext.getDataManager().markForUpdate(this);
		this.isPlayerGrid = isPlayerGrid;
	}

	/**
	 * @param localCoord
	 *            the localCoord to set
	 */
	public void setLocalCoord(Point localCoord) {
		AppContext.getDataManager().markForUpdate(this);
		this.localCoord = localCoord;
	}

	/**
	 * @param localIndex
	 *            the localIndex to set
	 */
	public void setLocalIndex(Point localIndex) {
		this.localIndex = new Point(localIndex.x, localIndex.y);
	}

	@Override
	public String toString() {
		return "background: " + this.background + " -- Coords: "
				+ this.coord.toString();
	}
}
